import { expect } from "chai";
import { ethers } from "ethers";
import { suite, test } from "mocha";
import * as sinon from "sinon";
import { FulfillmentManager } from "../../src/sdk/fulfillment";
import { Chain, EventType } from "../../src/types";
import { createMockContext } from "../fixtures/context";
import { mockListing, mockListingPartiallyFilled } from "../fixtures/listings";
import {
  mockOrderV2,
  mockOrderComponents,
  mockOfferOrderV2,
  mockPrivateListingOrderV2,
} from "../fixtures/orders";

suite("SDK: FulfillmentManager", () => {
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  let mockOrdersManager: any;
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  let mockAPI: any;
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  let mockSeaport: any;
  let mockDispatch: sinon.SinonStub;
  let mockConfirmTransaction: sinon.SinonStub;
  let mockRequireAccountIsAvailable: sinon.SinonStub;
  let fulfillmentManager: FulfillmentManager;

  const mockTransaction = {
    hash: "0xTxHash",
    wait: sinon.stub().resolves({ hash: "0xTxHash" }),
  };

  const mockSigner = {
    sendTransaction: sinon.stub().resolves({ hash: "0xFulfillTxHash" }),
  };

  beforeEach(() => {
    // Reset stubs
    mockSigner.sendTransaction = sinon
      .stub()
      .resolves({ hash: "0xFulfillTxHash" });

    // Mock OrdersManager
    mockOrdersManager = {
      buildListingOrderComponents: sinon.stub().resolves(mockOrderComponents),
      buildOfferOrderComponents: sinon.stub().resolves(mockOrderComponents),
    };

    // Mock OpenSeaAPI with full transaction data
    mockAPI = {
      generateFulfillmentData: sinon.stub().resolves({
        fulfillment_data: {
          transaction: {
            to: "0xSeaportAddress",
            value: 0,
            function:
              "fulfillAdvancedOrder(((address,address,(uint8,address,uint256,uint256,uint256)[],(uint8,address,uint256,uint256,uint256,address)[],uint8,uint256,uint256,bytes32,uint256,bytes32,uint256),uint120,uint120,bytes,bytes),(uint256,uint8,uint256,uint256,bytes32[])[],bytes32,address)",
            input_data: {
              advancedOrder: {
                parameters: {
                  offerer: "0xfba662e1a8e91a350702cf3b87d0c2d2fb4ba57f",
                  zone: "0x0000000000000000000000000000000000000000",
                  offer: [
                    {
                      itemType: 3,
                      token: "0x88d381e3c65221abea498c69e990d1deb7bd3863",
                      identifierOrCriteria: "1",
                      startAmount: "10000",
                      endAmount: "10000",
                    },
                  ],
                  consideration: [
                    {
                      itemType: 0,
                      token: "0x0000000000000000000000000000000000000000",
                      identifierOrCriteria: "0",
                      startAmount: "99000000",
                      endAmount: "99000000",
                      recipient: "0xfba662e1a8e91a350702cf3b87d0c2d2fb4ba57f",
                    },
                  ],
                  orderType: 1,
                  startTime: "1759963495",
                  endTime: "1775515495",
                  zoneHash:
                    "0x0000000000000000000000000000000000000000000000000000000000000000",
                  salt: "27855337018906766782546881864045825683096516384821792734247163280454785126732",
                  conduitKey:
                    "0x0000007b02230091a7ed01230072f7006a004d60a8d4e71d599b8104250f0000",
                  totalOriginalConsiderationItems: "2",
                },
                numerator: 1,
                denominator: 1,
                signature: "0x",
                extraData: "0x",
              },
              criteriaResolvers: [],
              fulfillerConduitKey:
                "0x0000000000000000000000000000000000000000000000000000000000000000",
              recipient: "0x0000000000000000000000000000000000000000",
            },
          },
          orders: [{ signature: "0xNewSignature" }],
        },
      }),
    };

    // Mock Seaport
    mockSeaport = {
      fulfillOrder: sinon.stub().returns({
        executeAllActions: sinon.stub().resolves("0xFulfillTxHash"),
      }),
      matchOrders: sinon.stub().returns({
        transact: sinon.stub().resolves(mockTransaction),
      }),
      validate: sinon.stub().returns({
        staticCall: sinon.stub().resolves(true),
        transact: sinon.stub().resolves(mockTransaction),
      }),
    };

    // Mock callback functions
    mockDispatch = sinon.stub();
    mockConfirmTransaction = sinon.stub().resolves();
    mockRequireAccountIsAvailable = sinon.stub().resolves();

    // Create SDKContext mock using fixture
    const mockContext = createMockContext({
      chain: Chain.Mainnet,
      api: mockAPI,
      seaport: mockSeaport,
      dispatch: mockDispatch,
      confirmTransaction: mockConfirmTransaction,
      requireAccountIsAvailable: mockRequireAccountIsAvailable,
      signerOrProvider: mockSigner,
    });

    // Create FulfillmentManager instance
    fulfillmentManager = new FulfillmentManager(mockContext, mockOrdersManager);
  });

  afterEach(() => {
    sinon.restore();
  });

  suite("fulfillOrder", () => {
    test("fulfills a listing order successfully", async () => {
      const result = await fulfillmentManager.fulfillOrder({
        order: mockOrderV2,
        accountAddress: "0xBuyer",
      });

      expect(mockRequireAccountIsAvailable.calledOnce).to.be.true;
      expect(mockAPI.generateFulfillmentData.calledOnce).to.be.true;
      expect(mockSigner.sendTransaction.calledOnce).to.be.true;
      expect(mockConfirmTransaction.calledOnce).to.be.true;
      expect(result).to.equal("0xFulfillTxHash");
    });

    test("fulfills an offer order successfully", async () => {
      const result = await fulfillmentManager.fulfillOrder({
        order: mockOfferOrderV2,
        accountAddress: "0xSeller",
      });

      expect(mockSigner.sendTransaction.calledOnce).to.be.true;
      expect(result).to.equal("0xFulfillTxHash");
    });

    test("fulfills criteria order with contract and tokenId", async () => {
      await fulfillmentManager.fulfillOrder({
        order: mockOrderV2,
        accountAddress: "0xBuyer",
        assetContractAddress: "0xNFT",
        tokenId: "123",
      });

      expect(mockAPI.generateFulfillmentData.calledOnce).to.be.true;
      const apiCall = mockAPI.generateFulfillmentData.firstCall.args;
      expect(apiCall[4]).to.equal("0xNFT");
      expect(apiCall[5]).to.equal("123");
    });

    test("includes extraData when order has offer protection", async () => {
      mockAPI.generateFulfillmentData.resolves({
        fulfillment_data: {
          transaction: {
            to: "0xSeaportAddress",
            value: 0,
            function:
              "fulfillAdvancedOrder(((address,address,(uint8,address,uint256,uint256,uint256)[],(uint8,address,uint256,uint256,uint256,address)[],uint8,uint256,uint256,bytes32,uint256,bytes32,uint256),uint120,uint120,bytes,bytes),(uint256,uint8,uint256,uint256,bytes32[])[],bytes32,address)",
            input_data: {
              advancedOrder: {
                parameters: {
                  offerer: "0xfba662e1a8e91a350702cf3b87d0c2d2fb4ba57f",
                  zone: "0x0000000000000000000000000000000000000000",
                  offer: [
                    {
                      itemType: 3,
                      token: "0x88d381e3c65221abea498c69e990d1deb7bd3863",
                      identifierOrCriteria: "1",
                      startAmount: "10000",
                      endAmount: "10000",
                    },
                  ],
                  consideration: [
                    {
                      itemType: 0,
                      token: "0x0000000000000000000000000000000000000000",
                      identifierOrCriteria: "0",
                      startAmount: "99000000",
                      endAmount: "99000000",
                      recipient: "0xfba662e1a8e91a350702cf3b87d0c2d2fb4ba57f",
                    },
                  ],
                  orderType: 1,
                  startTime: "1759963495",
                  endTime: "1775515495",
                  zoneHash:
                    "0x0000000000000000000000000000000000000000000000000000000000000000",
                  salt: "27855337018906766782546881864045825683096516384821792734247163280454785126732",
                  conduitKey:
                    "0x0000007b02230091a7ed01230072f7006a004d60a8d4e71d599b8104250f0000",
                  totalOriginalConsiderationItems: "2",
                },
                numerator: 1,
                denominator: 1,
                signature: "0x",
                extraData: "0x1234567890abcdef",
              },
              criteriaResolvers: [],
              fulfillerConduitKey:
                "0x0000000000000000000000000000000000000000000000000000000000000000",
              recipient: "0x0000000000000000000000000000000000000000",
            },
          },
          orders: [{ signature: "0xNewSignature" }],
        },
      });

      await fulfillmentManager.fulfillOrder({
        order: mockOrderV2,
        accountAddress: "0xBuyer",
      });

      expect(mockSigner.sendTransaction.calledOnce).to.be.true;
    });

    test("fulfills order with recipient address", async () => {
      const recipientAddress = "0xRecipient123";
      await fulfillmentManager.fulfillOrder({
        order: mockOrderV2,
        accountAddress: "0xBuyer",
        recipientAddress,
      });

      expect(mockAPI.generateFulfillmentData.calledOnce).to.be.true;
      const apiCall = mockAPI.generateFulfillmentData.firstCall.args;
      expect(apiCall[7]).to.equal(recipientAddress);
    });

    test("encodes fulfillBasicOrder alias using supported fragment", async () => {
      mockAPI.generateFulfillmentData.resolves({
        fulfillment_data: {
          transaction: {
            to: "0xSeaportAddress",
            value: 0,
            function: "fulfillBasicOrder_efficient_6GL6yc((uint256))",
            input_data: {
              basicOrderParameters: {
                offerer: "0xOfferer",
                zone: "0x0000000000000000000000000000000000000000",
              },
            },
          },
          orders: [{ signature: "0xAliasSignature" }],
        },
      });

      const encodeStub = sinon.stub(
        ethers.Interface.prototype,
        "encodeFunctionData",
      );
      encodeStub.callsFake(() => "0xAliasEncoded");

      try {
        const result = await fulfillmentManager.fulfillOrder({
          order: mockOrderV2,
          accountAddress: "0xBuyer",
        });

        expect(result).to.equal("0xFulfillTxHash");
        expect(mockSigner.sendTransaction.calledOnce).to.be.true;
        expect(encodeStub.firstCall.args[0]).to.equal("fulfillBasicOrder");
        expect(mockSigner.sendTransaction.firstCall.args[0].data).to.equal(
          "0xAliasEncoded",
        );
      } finally {
        encodeStub.restore();
      }
    });

    test("fulfills private listing successfully", async () => {
      const result = await fulfillmentManager.fulfillOrder({
        order: mockPrivateListingOrderV2,
        accountAddress: "0xPrivateBuyer",
      });

      expect(mockSeaport.matchOrders.calledOnce).to.be.true;
      expect(mockSeaport.fulfillOrder.called).to.be.false;
      expect(result).to.equal("0xTxHash");
    });

    test("throws when account is not available", async () => {
      mockRequireAccountIsAvailable.rejects(new Error("Account not available"));

      try {
        await fulfillmentManager.fulfillOrder({
          order: mockOrderV2,
          accountAddress: "0xBuyer",
        });
        expect.fail("Expected error to be thrown");
      } catch (error) {
        expect((error as Error).message).to.include("Account not available");
      }
    });

    test("throws when protocol is invalid", async () => {
      const invalidOrder = {
        ...mockOrderV2,
        protocolAddress: "0xInvalidProtocol",
      };

      try {
        await fulfillmentManager.fulfillOrder({
          order: invalidOrder,
          accountAddress: "0xBuyer",
        });
        expect.fail("Expected error to be thrown");
      } catch (error) {
        expect((error as Error).message).to.include("Unsupported protocol");
      }
    });

    test("handles transaction response as ContractTransactionResponse", async () => {
      const result = await fulfillmentManager.fulfillOrder({
        order: mockOrderV2,
        accountAddress: "0xBuyer",
      });

      expect(result).to.equal("0xFulfillTxHash");
    });
  });

  suite("fulfillOrder with remaining_quantity", () => {
    test("defaults to 1 when unitsToFill not specified for Listing", async () => {
      await fulfillmentManager.fulfillOrder({
        order: mockListing,
        accountAddress: "0xBuyer",
      });

      // SDK defaults unitsToFill to "1" for both listings and offers
      expect(mockSigner.sendTransaction.calledOnce).to.be.true;
      const apiCall = mockAPI.generateFulfillmentData.firstCall.args;
      expect(apiCall[6]).to.equal("1"); // unitsToFill defaults to "1"
    });

    test("defaults to 1 when unitsToFill not specified for partially filled Listing", async () => {
      await fulfillmentManager.fulfillOrder({
        order: mockListingPartiallyFilled,
        accountAddress: "0xBuyer",
      });

      // SDK defaults unitsToFill to "1" for both listings and offers
      expect(mockSigner.sendTransaction.calledOnce).to.be.true;
      const apiCall = mockAPI.generateFulfillmentData.firstCall.args;
      expect(apiCall[6]).to.equal("1"); // unitsToFill defaults to "1"
    });

    test("defaults to 1 when unitsToFill not specified for OrderV2", async () => {
      await fulfillmentManager.fulfillOrder({
        order: mockOrderV2,
        accountAddress: "0xBuyer",
      });

      // SDK defaults unitsToFill to "1" for both listings and offers
      expect(mockSigner.sendTransaction.calledOnce).to.be.true;
      const apiCall = mockAPI.generateFulfillmentData.firstCall.args;
      expect(apiCall[6]).to.equal("1"); // unitsToFill defaults to "1"
    });

    test("passes unitsToFill when specified", async () => {
      const orderWithoutRemainingQty = {
        order_hash: "0x789",
        chain: "ethereum",
        protocol_data: {
          parameters: mockOrderComponents,
          signature: "0xSignature",
        },
        protocol_address: mockOrderV2.protocolAddress,
        price: {
          currency: "ETH",
          decimals: 18,
          value: "1000000000000000000",
        },
      };

      await fulfillmentManager.fulfillOrder({
        order: orderWithoutRemainingQty,
        accountAddress: "0xBuyer",
        unitsToFill: 5,
      });

      expect(mockSigner.sendTransaction.calledOnce).to.be.true;
      const apiCall = mockAPI.generateFulfillmentData.firstCall.args;
      expect(apiCall[6]).to.equal("5"); // unitsToFill passed to API
    });
  });

  suite("isOrderFulfillable", () => {
    test("returns true when order is fulfillable", async () => {
      const result = await fulfillmentManager.isOrderFulfillable({
        order: mockOrderV2,
        accountAddress: "0xBuyer",
      });

      expect(mockSeaport.validate.calledOnce).to.be.true;
      expect(result).to.be.true;
    });

    test("returns false when order is not fulfillable", async () => {
      mockSeaport.validate.returns({
        staticCall: sinon.stub().resolves(false),
      });

      const result = await fulfillmentManager.isOrderFulfillable({
        order: mockOrderV2,
        accountAddress: "0xBuyer",
      });

      expect(result).to.be.false;
    });

    test("returns false on CALL_EXCEPTION error", async () => {
      const error = new Error("CALL_EXCEPTION") as unknown as {
        code: string;
        message: string;
      };
      error.code = "CALL_EXCEPTION";

      mockSeaport.validate.returns({
        staticCall: sinon.stub().rejects(error),
      });

      const result = await fulfillmentManager.isOrderFulfillable({
        order: mockOrderV2,
        accountAddress: "0xBuyer",
      });

      expect(result).to.be.false;
    });

    test("throws other errors", async () => {
      mockSeaport.validate.returns({
        staticCall: sinon.stub().rejects(new Error("Unknown error")),
      });

      try {
        await fulfillmentManager.isOrderFulfillable({
          order: mockOrderV2,
          accountAddress: "0xBuyer",
        });
        expect.fail("Expected error to be thrown");
      } catch (error) {
        expect((error as Error).message).to.include("Unknown error");
      }
    });

    test("throws when protocol is invalid", async () => {
      const invalidOrder = {
        ...mockOrderV2,
        protocolAddress: "0xInvalidProtocol",
      };

      try {
        await fulfillmentManager.isOrderFulfillable({
          order: invalidOrder,
          accountAddress: "0xBuyer",
        });
        expect.fail("Expected error to be thrown");
      } catch (error) {
        expect((error as Error).message).to.include("Unsupported protocol");
      }
    });
  });

  suite("approveOrder", () => {
    test("approves an order successfully", async () => {
      const result = await fulfillmentManager.approveOrder(mockOrderV2);

      expect(mockRequireAccountIsAvailable.calledOnce).to.be.true;
      expect(mockDispatch.calledOnce).to.be.true;
      expect(mockDispatch.firstCall.args[0]).to.equal(EventType.ApproveOrder);
      expect(mockSeaport.validate.calledOnce).to.be.true;
      expect(mockConfirmTransaction.calledOnce).to.be.true;
      expect(result).to.equal("0xTxHash");
    });

    test("approves order with domain", async () => {
      await fulfillmentManager.approveOrder(mockOrderV2, "opensea.io");

      const validateCall = mockSeaport.validate.firstCall.args;
      expect(validateCall[2]).to.equal("opensea.io");
    });

    test("dispatches ApproveOrder event", async () => {
      await fulfillmentManager.approveOrder(mockOrderV2);

      expect(mockDispatch.calledOnce).to.be.true;
      const eventData = mockDispatch.firstCall.args[1];
      expect(eventData.orderV2).to.equal(mockOrderV2);
      expect(eventData.accountAddress).to.equal("0xMaker");
    });

    test("throws when account is not available", async () => {
      mockRequireAccountIsAvailable.rejects(new Error("Account not available"));

      try {
        await fulfillmentManager.approveOrder(mockOrderV2);
        expect.fail("Expected error to be thrown");
      } catch (error) {
        expect((error as Error).message).to.include("Account not available");
      }
    });

    test("throws when protocol is invalid", async () => {
      const invalidOrder = {
        ...mockOrderV2,
        protocolAddress: "0xInvalidProtocol",
      };

      try {
        await fulfillmentManager.approveOrder(invalidOrder);
        expect.fail("Expected error to be thrown");
      } catch (error) {
        expect((error as Error).message).to.include("Unsupported protocol");
      }
    });
  });

  suite("validateOrderOnchain", () => {
    test("validates order components onchain successfully", async () => {
      const result = await fulfillmentManager.validateOrderOnchain(
        mockOrderComponents,
        "0xValidator",
      );

      expect(mockRequireAccountIsAvailable.calledOnce).to.be.true;
      expect(mockDispatch.calledOnce).to.be.true;
      expect(mockDispatch.firstCall.args[0]).to.equal(EventType.ApproveOrder);
      expect(mockSeaport.validate.calledOnce).to.be.true;
      expect(mockConfirmTransaction.calledOnce).to.be.true;
      expect(result).to.equal("0xTxHash");
    });

    test("dispatches ApproveOrder event with order components", async () => {
      await fulfillmentManager.validateOrderOnchain(
        mockOrderComponents,
        "0xValidator",
      );

      const eventData = mockDispatch.firstCall.args[1];
      expect(eventData.orderV2.protocolData).to.equal(mockOrderComponents);
      expect(eventData.accountAddress).to.equal("0xValidator");
    });

    test("calls validate with correct parameters", async () => {
      await fulfillmentManager.validateOrderOnchain(
        mockOrderComponents,
        "0xValidator",
      );

      const validateCall = mockSeaport.validate.firstCall.args;
      expect(validateCall[0][0].parameters).to.equal(mockOrderComponents);
      expect(validateCall[0][0].signature).to.equal("0x");
      expect(validateCall[1]).to.equal("0xValidator");
    });

    test("throws when account is not available", async () => {
      mockRequireAccountIsAvailable.rejects(new Error("Account not available"));

      try {
        await fulfillmentManager.validateOrderOnchain(
          mockOrderComponents,
          "0xValidator",
        );
        expect.fail("Expected error to be thrown");
      } catch (error) {
        expect((error as Error).message).to.include("Account not available");
      }
    });
  });

  suite("createListingAndValidateOnchain", () => {
    test("creates and validates a listing successfully", async () => {
      const result = await fulfillmentManager.createListingAndValidateOnchain({
        asset: { tokenAddress: "0xNFT", tokenId: "123" },
        accountAddress: "0xSeller",
        amount: "1000000000000000000",
      });

      expect(mockOrdersManager.buildListingOrderComponents.calledOnce).to.be
        .true;
      expect(mockSeaport.validate.calledOnce).to.be.true;
      expect(result).to.equal("0xTxHash");
    });

    test("forwards all listing parameters", async () => {
      await fulfillmentManager.createListingAndValidateOnchain({
        asset: { tokenAddress: "0xNFT", tokenId: "123" },
        accountAddress: "0xSeller",
        amount: "2000000000000000000",
        quantity: 5,
        domain: "opensea.io",
        salt: "12345",
        listingTime: 1000000,
        expirationTime: 2000000,
        paymentTokenAddress: "0xUSDC",
        buyerAddress: "0xBuyer",
        includeOptionalCreatorFees: true,
        zone: "0xZone",
      });

      const buildCall =
        mockOrdersManager.buildListingOrderComponents.firstCall.args[0];
      expect(buildCall.asset.tokenAddress).to.equal("0xNFT");
      expect(buildCall.amount).to.equal("2000000000000000000");
      expect(buildCall.quantity).to.equal(5);
      expect(buildCall.domain).to.equal("opensea.io");
      expect(buildCall.buyerAddress).to.equal("0xBuyer");
    });
  });

  suite("createOfferAndValidateOnchain", () => {
    test("creates and validates an offer successfully", async () => {
      const result = await fulfillmentManager.createOfferAndValidateOnchain({
        asset: { tokenAddress: "0xNFT", tokenId: "123" },
        accountAddress: "0xBuyer",
        amount: "1000000000000000000",
      });

      expect(mockOrdersManager.buildOfferOrderComponents.calledOnce).to.be.true;
      expect(mockSeaport.validate.calledOnce).to.be.true;
      expect(result).to.equal("0xTxHash");
    });

    test("forwards all offer parameters", async () => {
      await fulfillmentManager.createOfferAndValidateOnchain({
        asset: { tokenAddress: "0xNFT", tokenId: "123" },
        accountAddress: "0xBuyer",
        amount: "1500000000000000000",
        quantity: 3,
        domain: "test.io",
        salt: "67890",
        expirationTime: 3000000,
        paymentTokenAddress: "0xWETH",
        zone: "0xSignedZone",
      });

      const buildCall =
        mockOrdersManager.buildOfferOrderComponents.firstCall.args[0];
      expect(buildCall.asset.tokenAddress).to.equal("0xNFT");
      expect(buildCall.amount).to.equal("1500000000000000000");
      expect(buildCall.quantity).to.equal(3);
      expect(buildCall.domain).to.equal("test.io");
      expect(buildCall.paymentTokenAddress).to.equal("0xWETH");
    });
  });

  suite("Constructor", () => {
    test("initializes with all required dependencies", () => {
      const mockContext = createMockContext({
        chain: Chain.Mainnet,
        api: mockAPI,
        seaport: mockSeaport,
        dispatch: mockDispatch,
        confirmTransaction: mockConfirmTransaction,
        requireAccountIsAvailable: mockRequireAccountIsAvailable,
      });

      const manager = new FulfillmentManager(mockContext, mockOrdersManager);

      expect(manager).to.be.instanceOf(FulfillmentManager);
    });
  });
});
